/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Scene architecture:
 *
 *            SRFile
 *            |     \
 *            v      v
 *    SRNamespace -> SRClass
 *                  /      |
 *                 v       v
 *            SRMethod   SRField
 *               |
 *               v
 *            SRStmt
 *               |
 *               v
 *            SRFieldRef
 */

export {
    SRClass,
    SRClassType,
    SRDecorator,
    TypeDecorator,
    ExposeDecorator,
    ExcludeDecorator,
    SRDiscriminator,
    SRPassThrough,
    SRPostProcess,
    SubDiscriminatorType,
    SRExposeName,
    SROption,
    UnclearDecorator,
    SRField,
    BaseType,
    FieldType,
    SRFile,
    SRMethod,
    SRNamespace,
    SRScene,
    SRStmt,
    SRFieldRef,
    SRImportElement,
    THIS,
};

/*
 * Save Decorator information
 */
abstract class SRDecorator {
    // Indicated type in decorator
    // e.g. @Type(() => TestA)
    //  type = 'TestA'
    private type: string;

    constructor(t: string) {
        this.type = t;
    }

    public getType(): string {
        return this.type;
    }

    public setType(value: string): void {
        this.type = value;
    }
}

class TypeDecorator extends SRDecorator {
    private option: SROption | undefined

    constructor(t: string, discriminator: SROption | undefined) {
        super(t)
        this.option = discriminator
    }

    public getDiscriminator(): SROption {
        return this.option!
    }

    public setDiscriminator(option: SROption): void {
        this.option = option
    }
}

class ExposeDecorator extends SRDecorator {
    private name: SRExposeName
    
    constructor(t: string, name: SRExposeName) {
        super(t)
        this.name = name
    }

    public getName(): SRExposeName {
        return this.name
    }

    public setDiscriminator(name: SRExposeName): void {
        this.name = name
    }
}

class ExcludeDecorator extends SRDecorator {
    constructor(t: string) {
        super(t)
    }
}

class UnclearDecorator extends SRDecorator {
    constructor(t: string) {
        super(t)
    }
}

/**
 * decorator options
 */
type SubDiscriminatorType = {
    name: string | number,
    value: SRClass
}
abstract class SROption {

}

class SRDiscriminator extends SROption {
    private property: string
    private subTypes: SubDiscriminatorType[]

    constructor(property: string, subTypes: SubDiscriminatorType[]) {
        super()
        this.property = property
        this.subTypes = subTypes
    }

    public getProperty(): string {
        return this.property;
    }

    public getSubTypes(): SubDiscriminatorType[] {
        return this.subTypes;
    }
}

class SRPassThrough extends SROption {
    private value: boolean

    constructor(value: boolean) {
        super()
        this.value = value
    }
}

class SRPostProcess extends SROption {
    private value: boolean

    constructor(value: boolean) {
        super()
        this.value = value
    }
}

class SRExposeName extends SROption {
    private value: string

    constructor(value: string) {
        super()
        this.value = value
    }

    public getName(): string {
        return this.value;
    }
}

class SRImportElement {
    // import element
    private elementName: string;
    // whether import has bracket
    private namedImport: boolean;

    constructor(elementName: string, namedImport: boolean) {
        this.elementName = elementName;
        this.namedImport = namedImport;
    }

    public getElementName(): string {
        return this.elementName;
    }

    public isNamedImport(): boolean {
        return this.namedImport;
    }

    public equals(other: SRImportElement): boolean {
        return this.elementName === other.elementName && this.namedImport === other.namedImport;
    }
}

/* All of field types except for OTHERS is the allownce type for SR refactor
 *
 */
enum BaseType { BOOLEAN, NUMBER, BIGINIT, STRING,
     ARRAY, SET, MAP, COLLECTIONS_ARRAY, COLLECTIONS_SET, COLLECTIONS_MAP, 
     ENUM, CLASS, OBJECT, UNION, OTHERS }

type FieldType = {
    category: BaseType,
    name: string,
    genericTypes: FieldType[]
}

/*
 * Class field
 */
class SRField {
    // Field type
    private type: FieldType;
    // Field name
    private name: string;
    // Has ? in origin code
    private _isOptional: boolean;
    // Initializer for a field in string format
    private initializer: string;
    // Decorators
    private decorators: SRDecorator[] = [];
    // modifiers, include static, public ...
    private modifiers: string[] = [];
    private declaringClass: SRClass

    constructor(n: string) {
        this.name = n;
    }

    public getType(): FieldType {
        return this.type;
    }

    public setType(t: FieldType): void {
        this.type = t;
    }

    public getName(): string {
        return this.name;
    }

    public setName(n: string): void {
        this.name = n;
    }

    public isOptional(): boolean {
        return this._isOptional;
    }

    public setIsOptional(isOptional: boolean): void {
        this._isOptional = isOptional;
    }

    public getInitializer(): string {
        return this.initializer;
    }

    public setInitializer(i: string): void {
        this.initializer = i;
    }

    public static extractRightSideOfAssignment(codeLine: string): string {
        // get the content after '='
        const match = codeLine.match(/=(.*)\s*;?\s*$/);
        return match ? match[1].trim() : '';
    }

    public getDecorators(): SRDecorator[] {
        return this.decorators;
    }

    public addDecorator(decorator: SRDecorator): void {
        this.decorators.push(decorator);
    }

    public isStatic(): boolean {
        for (let modifier in this.modifiers) {
            if (modifier == 'StaticKeyword') {
                return true;
            }
        }
        return false;
    }

    public getModifiers(): string[] {
        return this.modifiers;
    }

    public addModifier(modifiers: string) {
        this.modifiers.push(modifiers);
    }

    public setDeclaringClass(srClass: SRClass) {
        this.declaringClass = srClass
    }

    public getDeclaringClass(): SRClass {
        return this.declaringClass
    }
}

const THIS = 'this';

/*
 * FieldRef in statement
 */
class SRFieldRef {
    private base: string;
    private name: string;

    constructor(name: string, base: string = THIS) {
        this.name = name;
        this.base = base;
    }

    public getName(): string {
        return this.name;
    }

    public getBase(): string {
        return this.base;
    }

    public toString(): string {
        return this.base + '.' + this.name;
    }
}

/*
 * Statement in method
 */
class SRStmt {
    // Field or local variable of Operands in a statement
    private fieldRefs: SRFieldRef[] = [];
    // Original codes
    private originalCode: string;

    constructor(originalCode: string) {
        this.originalCode = originalCode;
    }

    public getFieldRefs(): SRFieldRef[] {
        return this.fieldRefs;
    }

    public setFieldRefs(fieldRefs: SRFieldRef[]): void {
        this.fieldRefs = fieldRefs;
    }

    public getOriginalCode(): string {
        return this.originalCode;
    }

    public setOriginalCode(code: string): void {
        this.originalCode = code;
    }

    public addFieldRef(fieldRef: SRFieldRef): void {
        this.fieldRefs.push(fieldRef);
    }
}

/*
 * Method
 */
class SRMethod {
    // Name
    private name: string;
    // Return Type
    private returnType: string;
    // Parameters in string format
    private parameters: string[] = [];
    // modifiers
    private modifiers: string[] = [];
    // Full original codes of this method
    private originalCode: string;
    // statements of this method
    private statements: SRStmt[] = [];

    constructor(name: string) {
        this.name = name;
    }

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        this.name = name;
    }

    public getReturnType(): string {
        return this.returnType;
    }

    public setReturnType(returnType: string): void {
        this.returnType = returnType;
    }

    public getParameters(): string[] {
        return this.parameters;
    }

    public setParameters(parameters: string[]): void {
        this.parameters = parameters;
    }

    public isStatic(): boolean {
        for (let modifier in this.modifiers) {
            if (modifier == 'StaticKeyword') {
                return true;
            }
        }
        return false;
    }

    public getModifiers(): string[] {
        return this.modifiers;
    }

    public setModifiers(modifiers: string[]) {
        this.modifiers = modifiers;
    }

    public getOriginalCode(): string {
        return this.originalCode;
    }

    public setOriginalCode(originalCode: string): void {
        this.originalCode = originalCode;
    }

    public getStatements(): SRStmt[] {
        return this.statements;
    }

    public setStatements(statements: SRStmt[]) {
        this.statements = statements;
    }
}

/*
 * Namespace
 */
class SRNamespace {
    // Name
    private name: string;
    // Default class use to put the non-member functions (not in a class)
    private defaultClass: SRClass;
    // Classes in the namespace
    private classes: SRClass[] = [];
    // namespaces in this namespace
    private namespaces: SRNamespace[] = [];

    constructor(name: string) {
        this.name = name;
    }

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        this.name = name;
    }

    public getDefaultClass(): SRClass {
        return this.defaultClass;
    }

    public setDefaultClass(defaultClass: SRClass): void {
        this.defaultClass = defaultClass;
    }

    public getClasses(): SRClass[] {
        return this.classes;
    }

    public addClass(srclass: SRClass): void {
        this.classes.push(srclass);
    }

    public setClasses(classes: SRClass[]): void {
        this.classes = classes;
    }

    public getNamespaces(): SRNamespace[] {
        return this.namespaces;
    }

    public addNamespace(srNamespace: SRNamespace): void {
        this.namespaces.push(srNamespace);
    }

    public setNamespaces(namespaces: SRNamespace[]): void {
        this.namespaces = namespaces;
    }
}

/*
 * Class types we supported
 */
enum SRClassType { INTERFACE, CLASS, STRUCT, ENUM, OBJECT}

/*
 * Class
 */
class SRClass {
    // Name
    private name: string;
    // Class type
    private type: SRClassType;
    private originalCode: string
    // Decorators
    private decorators: SRDecorator[] = [];
    private modifiers: string[] = [];
    private implementClasses: SRClass[] = [];
    // Father class
    private superClass: SRClass;
    // Methods in this class
    private methods: SRMethod[] = [];
    // Fields in this class
    private fields: SRField[] = [];
    private declaringFile: SRFile;

    constructor(name: string, type: SRClassType) {
        this.name = name;
        this.type = type;
    }

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        this.name = name;
    }

    public getType(): SRClassType {
        return this.type;
    }

    public setType(type: SRClassType): void {
        this.type = type;
    }

    public getDecorators(): SRDecorator[] {
        return this.decorators;
    }

    public addDecorator(decorator: SRDecorator): void {
        this.decorators.push(decorator);
    }

    public setDecorators(decorators: SRDecorator[]): void {
        this.decorators = decorators;
    }

    public getSuperClass(): SRClass {
        return this.superClass;
    }

    public setSuperClass(superClass: SRClass): void {
        this.superClass = superClass;
    }

    public getMethods(): SRMethod[] {
        return this.methods;
    }

    public addMethod(method: SRMethod): void {
        this.methods.push(method);
    }

    public setMethods(methods: SRMethod[]): void {
        this.methods = methods;
    }

    public getFields(): SRField[] {
        return this.fields;
    }

    public addField(field: SRField): void {
        this.fields.push(field);
    }

    public setFields(fields: SRField[]): void {
        this.fields = fields;
    }

    public addModifier(modifier: string): void {
        this.modifiers.push(modifier);
    }

    public getModifiers(): string[] {
        return this.modifiers;
    }

    public addImplementedClass(srClass: SRClass): void {
        this.implementClasses.push(srClass);
    }

    public setDeclaringFile(srFile: SRFile) {
        this.declaringFile = srFile
    }

    public getDeclaringFile(): SRFile {
        return this.declaringFile
    }

    public getOriginalCode(): string {
        return this.originalCode
    }

    public setOriginalCode(code: string) {
        this.originalCode = code
    }
}

/*
 * File
 */
class SRFile {
    // Name
    private name: string;
    // File's original absolute path
    private filePath: string;
    // Default class to put the non-member functions (not in a class)
    private defaultClass: SRClass;
    // Classes in this file
    private classes: SRClass[] = [];
    // Namespaces in this file
    private namespaces: SRNamespace[] = [];
    // Imports in this file
    private importInfoMap: Map<string, SRImportElement[]> = new Map<string, SRImportElement[]>();

    constructor(name: string) {
        this.name = name;
    }

    public getName(): string {
        return this.name;
    }

    public setName(name: string): void {
        this.name = name;
    }

    public getFilePath(): string {
        return this.filePath;
    }

    public setFilePath(filePath: string): void {
        this.filePath = filePath;
    }

    public getDefaultClass(): SRClass {
        return this.defaultClass;
    }

    public setDefaultClass(defaultClass: SRClass): void {
        this.defaultClass = defaultClass;
    }

    public getClasses(): SRClass[] {
        return this.classes;
    }

    public addClass(srclass: SRClass): void {
        this.classes.push(srclass);
    }

    public setClasses(classes: SRClass[]): void {
        this.classes = classes;
    }

    public getNamespaces(): SRNamespace[] {
        return this.namespaces;
    }

    public addNamespace(srNamespace: SRNamespace): void {
        this.namespaces.push(srNamespace);
    }

    public setNamespaces(namespaces: SRNamespace[]): void {
        this.namespaces = namespaces;
    }

    public getImportInfoMap(): Map<string, SRImportElement[]> {
        return this.importInfoMap;
    }

    public getImportInfoBy(name: string): SRImportElement[] | undefined {
        return this.importInfoMap.get(name);
    }

    public addImportInfo(importSource: string, elementName: string, namedImport: boolean) {
        if (!this.importInfoMap.has(importSource)) {
            this.importInfoMap.set(importSource, []);
        }
        if (elementName !== "") {
            const importElements = this.importInfoMap.get(importSource)!;
            let importElement = new SRImportElement(elementName, namedImport);
            let alreadyExists = false;
            for (let element of importElements) {
                if (element.equals(importElement)) {
                    alreadyExists = true;
                    break;
                }
            }
            if (!alreadyExists) {
                importElements.push(importElement);
            }
        }
    }

    public getClassByName(name:string): SRClass | undefined {
        return this.classes.find(srClass => srClass.getName() === name)
    }
}

/*
 * Scene
 */
class SRScene {
    // All of SRFiles
    private files: SRFile[] = [];

    constructor() {
    }

    public getFiles(): SRFile[] {
        return this.files;
    }

    public addFile(srFile: SRFile): void {
        this.files.push(srFile);
    }

    public setFiles(files: SRFile[]): void {
        this.files = files;
    }

    public findFileByName(fileName: string): SRFile | undefined {
        return this.files.find(file => file.getName() === fileName);
    }

    public findFileByPath(filePath: string): SRFile | undefined {
        return this.files.find(file => file.getFilePath() === filePath);
    }
}